﻿using System.IO;
using System.Xml;
using System.Xml.Linq;
using System.Xml.XPath;

using Microsoft.WowAddonStudio.Lua.Model;
using Microsoft.WowAddonStudio.Lua.Parser;

namespace Microsoft.WowAddonStudio.Lua.Loaders
{
    public class FrameXmlFileDocumentationLoader : FileDocumentationLoader
    {
        private static readonly XmlNamespaceManager namespaceManager;

        private const string widgetDeclarationExpression = "/wow:Ui//*[@name]";
        private const string scriptReferenceExpression = "/wow:Ui/wow:Script[@file]";
        private const string inlineScriptExpression = "/wow:Ui/wow:Script[not(@file)]";
        private const string eventHandlerScriptExpression = "/wow:Ui//wow:Scripts/*";

        public delegate void AddReferencedScriptDelegate(DocumentationScope documentationScope, string referencedScriptPath);

        /// <summary>
        /// Gets or sets the delegate used to handle external script references.
        /// </summary>
        public AddReferencedScriptDelegate ScriptReferenceHandler { get; set; }

        /// <summary>
        /// Initializes the <see cref="FrameXmlFileDocumentationLoader"/> class.
        /// </summary>
        static FrameXmlFileDocumentationLoader()
        {
            namespaceManager = new XmlNamespaceManager(new NameTable());
            namespaceManager.AddNamespace("wow", "http://www.blizzard.com/wow/ui/");
        }

        /// <summary>
        /// Loads the documentation from a file.
        /// </summary>
        /// <param name="fileStream">A <see cref="Stream"/> to the file.</param>
        /// <returns>An instance of the <see cref="DocumentationScope"/> class.</returns>
        public override DocumentationScope LoadFileDocumentation(Stream fileStream)
        {
            var fileDocumentationScope = new DocumentationScope(new DocumentationScope());

            var frameXmlDocument = XDocument.Load(XmlReader.Create(fileStream), LoadOptions.SetLineInfo);

            // Add script references through ScriptReferenced event
            AddScriptReferences(fileDocumentationScope, frameXmlDocument);

            // Add widget declarations
            AddWidgetDeclarations(fileDocumentationScope, frameXmlDocument);

            // Add inline script declarations and event handlers
            AddScriptDeclarations(fileDocumentationScope, frameXmlDocument, inlineScriptExpression);
            AddScriptDeclarations(fileDocumentationScope, frameXmlDocument, eventHandlerScriptExpression);

            return fileDocumentationScope;
        }

        private void AddScriptReferences(DocumentationScope fileDocumentationScope, XDocument frameXmlDocument)
        {
            foreach (var element in frameXmlDocument.XPathSelectElements(scriptReferenceExpression, namespaceManager))
            {
                var referencedScriptPath = (string)element.Attribute("file");
                if (ScriptReferenceHandler != null)
                    ScriptReferenceHandler(fileDocumentationScope, referencedScriptPath);
            }
        }

        private static void AddWidgetDeclarations(DocumentationScope fileDocumentationScope, XDocument frameXmlDocument)
        {
            // Find all elements that have name and do not have 'virtual' attribute
            foreach (var element in frameXmlDocument.XPathSelectElements(widgetDeclarationExpression, namespaceManager))
            {
                IXmlLineInfo lineInfo = element;

                fileDocumentationScope.Add(new Table
                                           {
                                               Location = new LexLocation(lineInfo.LineNumber, lineInfo.LinePosition),
                                               Name = (string)element.Attribute("name"),
                                               Prototype = element.Name.LocalName,
                                               Virtual = element.Attribute("virtual") != null && (string)element.Attribute("virtual") == "true"
                                           });
            }
        }

        private static void AddScriptDeclarations(DocumentationScope fileDocumentationScope, XDocument frameXmlDocument, string expression)
        {
            foreach (var element in frameXmlDocument.XPathSelectElements(expression, namespaceManager))
            {
                // Get the script content
                var script = element.Value;

                // Parse the script using a Lua parser
                var scriptScope = DocumentationParser.Parse(script);

                // Merge the script's global scope into the global scope
                fileDocumentationScope.GlobalScope.Merge(scriptScope.GlobalScope);

                // Parent the scriptScope to the documentation scope
                scriptScope.Parent = fileDocumentationScope;

                // Update the Location of the scriptScope
                IXmlLineInfo lineInfo = element;
                IXmlLineInfo endLineInfo = element.NextNode ?? (element.Parent != null ? element.Parent.NextNode : null);

                if (endLineInfo != null)
                    scriptScope.Location = new LexLocation(lineInfo.LineNumber, lineInfo.LinePosition, endLineInfo.LineNumber, endLineInfo.LinePosition);
            }
        }

    }
}
